Skip to content

feat(Lightbox): swipe between gallery photos like Photos - #8197

Merged
cixzhang merged 22 commits into
meta:mainfrom
vjeux:uikit/c24-pager
Nov 8, 2026
Merged

cixzhang merged 22 commits into
meta:mainfrom
vjeux:uikit/c24-pager

Conversation

@vjeux

@vjeux vjeux commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Batch: B7 Lightbox
Stack: #8197 → #9810 → #10284
Stack: #8197 → #8215

On main 5c1b7d4. This head reads a busy pool's left-out run as none, vjeux@0bb42a8: main's #10464 now leaves out a run that began too late, as null in the pool's runs. The page settle proof's judged runs must exist, and its delayed control fails by name when the pool left its first run out. Before it, 51bf10b merges main 5c1b7d4 with no conflict. The head before, 3963547, fixes the photo a page leaves behind, vjeux@3963547. The pager's settle reset every photo outside the page marked current. On a slow runner the settle ends before the render that moves that mark, so the photo left behind kept its zoom. Each page now carries its index, and the reset skips only the page the pager names. Before it, 17e7968 merges main 993e1fa with no conflict. The head before, cc382a8, merged main into e0eab2a again: main's #10415 plays the busy double-tap zoom proof in LightboxZoom.interaction.browser.spec.ts at tempo 5 through its own slowZoom, which this pull request replaced with the shared slowViewer. The merge keeps #10415's three constants and calls slowViewer(page, ZOOM_TEMPO) in the proof and its two controls; every other path is the automatic merge. The head before, e0eab2a, kept main's #10406 recordLiftOffset beside the slowViewer import, and d3bd5ce merged main into 258ac0d after main's v0.7.10 release consumed .changeset/lightbox-every-pointer.md, the note for #8148's ctrl-wheel and trackpad zoom. This pull request had edited that note to read "with hasZoom". The merge keeps main's deletion and drops the edit, because on main the zoom stage already runs only for a photo with hasZoom, so the words named no change of this pull request. Its own notes, lightbox-gallery-paging.md and pager-settle-tempo.md, are unchanged, and so is the rest of its diff. The head before, 258ac0d, added two commits to 3ee5207: a merge of main, which brings the harness's per-run judging of a pooled busy proof, and a fix to the busy proof of a page's settle, vjeux@258ac0d (see Evidence). This pull request's own diff is otherwise unchanged. The fixes from the rounds before stay: the release note, vjeux@32e21eb, the instant-page check's frame floor, vjeux@3ee5207, and vjeux@91d4a1e and vjeux@22da53d

The previous head added the zoom pan's begin threshold on the merge worker's 0fc7062 (see Evidence): vjeux@b8266e6

Until card 46 decides the shared caption row, each page lays out its own photo and caption as main does; a neighbour with a longer caption can arrive with a slightly smaller photo.

Drag paging is part of the touch presentation, which a coarse primary pointer brings (spec:AST-094 FR13), as the spec owners ruled for drag to close (#8105). Under a fine pointer the Previous and Next buttons and the arrow keys page, and a mouse's drag or a trackpad's sideways scroll leaves a gallery on its photo, as on main. FR11's text is unchanged; the record's Current-state list keeps naming FR7's drag paging and FR8's drag to close as not built for a mouse, a pen or a trackpad. This changes this pull request's own product lines after its stamp at d585c45, so it needs the spec owners' re-check.

With zoom, a page's caption moves up under its photo. A click or tap in the strip it leaves at the page's bottom edge closes the viewer, as on main: the stage's tap, which sees the gallery's whole track, now closes for any tap off the photo except on the caption.

Rebased on #8148's 14f14d7 (#8147 e463256 on main f10bc91), with no change to this pull request's own lines. Against 2b36122, the head the spec owners stamped:

  • A mouse's or a pen's click in a gallery closes only outside main's media group, by the rule feat(Lightbox): touch zoom like the iOS Photos viewer #8148 now follows. That group runs from the photo's top to its caption's bottom. It is as wide as the photo's natural width or its caption, whichever is wider, up to the viewer. So a click beside a large photo, level with it, leaves the viewer open, as on main, and a click past a narrow photo's caption closes it. A finger keeps the union box below, and the stage's backdrop tap takes only a finger.
  • Every press in the viewer records its pointer and point at the viewer's container, as feat(Lightbox): touch zoom like the iOS Photos viewer #8148 now does, so the stage's own handler goes.
  • Main's test(browser): keep every Playwright browser context off remote hosts #9811 makes @playwright/test a lint error in browser specs, so the Paging and PagingModes specs import test and expect from @astryxdesign/a11y-spec/playwright, as main's do.
  • While zoomed, the photo's shown box counts as media for a mouse's click, as feat(Lightbox): touch zoom like the iOS Photos viewer #8148 now rules, so a click on a zoomed page photo never closes the viewer.
  • The record's Current-state list: the paging motion roles line now names the page settle only, by the spec owners' ruling, and the press tracker line names where presses are recorded now, the viewer's pointerdown handler and the zoom stage's capture listener for the start threshold.
  • Two tests follow feat(Lightbox): swipe between gallery photos like Photos #8197's paging: a finger's drag on a fitted photo starts no zoom pan here, so the strip's first tap after it closes the viewer, as at 2b36122; and a gallery page's caption is found on its own page.

Rebased on #8148's 33e0568 (main 5a373df). Against d585c45, the head the spec owners stamped:

  • Drag paging is the touch presentation's (above): the pager's pan is on only under a coarse primary pointer.
  • useZoomStage reads its options directly in the tap callback, as feat(Lightbox): touch zoom like the iOS Photos viewer #8148 now does, since useGestureRecognizer calls the newest callback.
  • usePager keeps a refused drag's spring-back with isControlledChangeAnimated off. Lightbox no longer reaches that path (its pan is touch-only, where the option is on), so a hook test pins it: with the option off, a drag the owner refuses springs back animated, and a new controlled index moves at once. Its red-first is a NOT-FOR-LANDING arm (9dd2b84) where a refused page follows the option.
  • Without zoom, a mouse's click on the stage closes the viewer as a click on main's backdrop does. It closes when its press or its release is off the current media group: the photo or video and its caption, as one box. So a press on the dark area released on the photo closes, and a click beside a photo narrower than its caption, within the caption's width, leaves the viewer open. How far the press moved or how long it was held does not matter. The stage's tap recognizer (a 45 px movement limit and a 1.5 s hold limit) takes only a finger, and tests the same box. A keyboard's click on a caption's link stays the link's.
  • The props doc and the changeset say that a swipe pages on a phone or tablet, not under every pointer.
  • feat(Lightbox): touch zoom like the iOS Photos viewer #8148's every-pointer changeset says the ctrl-wheel and trackpad gestures come with hasZoom, since a Lightbox without it leaves them to the page.
  • Two comments that restated the code go, and two long sentences are split.

Against 39ad02a, where the iPhone check passed, this head answers the reviews of that head and of 5a34859:

  • A mouse's or the keyboard's Next shows the next photo at once again, as before this pull request; only a finger's swipe slides. The shared usePager hook gains isControlledChangeAnimated (default true, so Carousel is unchanged), and Lightbox passes isControlledChangeAnimated: isTouch.
  • A drag the owner refuses springs back under every pointer (FR7). The return after a page the pager reported and the owner refused animates whatever isControlledChangeAnimated says; only a new index follows it.
  • The press tracker goes through the shared recognizers (FR14). The video controls band reads where the pan's first touch began, and the caption pinch reads the pinch's first touch. Without zoom, a tap beside a gallery's photo closes the viewer through a shared tap recognizer, which fails past the tap's movement limit. The stage's own pointerdown listener now only records the pointer type the browser's dblclick does not carry.
  • A pinch that starts on a caption zooms the page, not the photo. The photo row takes its own touches, the track leaves only the page's pinch-zoom, and the zoom stage's pinch does not begin when its first finger lands on a caption. A one-finger swipe from the caption still pages.
  • A video that ends during a drag on its frame leaves the drag to page; a guard test holds it.
  • The record names two more Current-state gaps (below), and an unused usePager call and a stale comment go.

Batch: Lightbox · paging. One intent, gallery paging across pointers; most of its authored lines are tests (about 1,550 of 2,350).

Why

On a phone, a Lightbox gallery moves only through two small arrow buttons. People expect to swipe between photos as in Photos, with a zoomed photo panning to its edge before the next one comes.

What led to it

The UIKit-parity work for Astryx (spec:AST-094): after the zoom (#8148), paging, on the shared pager main already has. component:Lightbox FR7 (current) decides it.

Options and tradeoffs

  • CSS scroll snapping: native and small, but it cannot share a gesture with the pinch (a drifting pinch pages) or apply UIKit's release rule.
  • A pager on the gesture host (this pull request): the row follows the finger, the release picks the page by UIKit's rule (one page at most) and settles on the compositor, and zoom and paging share the drag as nested scroll views do. The cost is JavaScript; the size line is under Evidence.

Why this choice

Only the pager keeps a pinch from paging and lets a zoomed photo keep the drag until its edge, as Photos does. Pages sit 40 pt apart, Photos' measured gap.

  • A video pages from its frame. Its native controls are taken as a 48 px band at its bottom edge, since browsers expose no control geometry, so a drag that starts there stays the video's: a scrubber or a volume drag does not page. A browser that places its controls elsewhere pages from them; the record says so.

Record change

component:Lightbox's Current-state impact list gains a note on FR7, in the spec owner's words: "Video controls (FR7). A video's native controls are taken as a 48 px band at its bottom edge, since browsers expose no control geometry; a browser that places controls elsewhere pages from them." FR7's text is unchanged. It also gains two lines: "Paging motion roles (FR5). The page settle and the buttons' slide play the pager's own UIKit values, which the motion role table does not list as roles." and "Press tracker (FR14). The stage's own pointerdown listener records each press's pointer type, which the browser's dblclick does not carry, and clears the flag a drag leaves for its click." The list also drops what this change closes: swipe paging from the touch gaps, and paging from the fine-pointer gap ("A mouse drag on a fitted photo does not close the viewer" remains). The record needs a spec owner's approval.

FR11's text is unchanged. Its Current-state gap line stays as #8148 has it: "Fine-pointer gestures (FR11). FR11's drag paging (FR7) and drag to close (FR8) are not built for a mouse, a pen or a trackpad: a drag on a fitted photo does nothing."

Writing rubric results

Run over docs/specs/components/Lightbox/Lightbox.spec.md:

  • R1 narration: none; the Current-state lines state what the shipped component does.
  • R2 research: none in the record.
  • R3 evidence: none.
  • R4 rejected alternatives: one, in DEC-2, one line (unchanged).
  • R5 change references: none.
  • R6 open questions: none open.

How to check it

In the "Gallery with zoom" story, on a phone:

  1. Swipe left: the next photo slides in, and the counter reads 2 / 4.

  2. Drag a little and let go: the photo springs back.

  3. Zoom in, drag to the photo's edge, then drag again: the next photo comes.

  4. Pinch with a sideways drift: the photo zooms; nothing pages.

  5. In "Gallery with video", page to the video. Drag its scrubber: the video seeks and the gallery stays. Drag on the video's frame: the gallery pages.

  6. Pinch on a photo's caption: the page zooms and the photo stays as it is. Swipe from the caption: the gallery pages.

With a mouse, a sideways drag leaves the gallery on its photo, as on main, and so does a trackpad's sideways scroll. A click on Next shows the next photo at once. With Reduce Motion on, a swipe fades instead of sliding.

Evidence

  • The busy proof of a page's settle pools six settles (258ac0d): CI's WebKit run failed it on its own floor, "the control moved on 3 frames in the judged part, under 4", though the photo had moved on every frame the control moved. One settle moves visibly for about 550 ms at tempo 3, and with the main thread blocked WebKit's software compositor presents a frame every 110 to 200 ms, so one run held 3 to 5 frames. In the CI profile (4 CPUs) the old proof passed 10/10 at exactly 4 frames, failed 2/10 after a merge with main, and failed 6/6 with four busy threads in the container. The proof now plays six settles at tempo 5, to page 4 and back, and judges them as one pool (the harness's pooledBusyProof): a run whose control presented nothing is left out by name, and the pool needs three judged runs.

    • The frame in flight: in a trial with two busy threads in the container (165 to 190 ms a frame, the CI runner's cadence), every frame the photo missed was the first control frame after the block began (+20 ms), and the photo first moved 140 to 191 ms into the block, once 276 and once 420 ms. A starved compositor drew a frame it had begun before it applied the settle handed over just before the block. Each block now starts one rendering update after the handoff, since WebKit runs that update only once its compositor has drawn the last one, and each run skips at most one frame the compositor began before the block.
    • Controls: a settle the main thread drives, a frozen one, and a settle that stays still into the block for two frames and a little more, or for 450 ms, all fail the proof by name.
    • WebKit (CI profile): 9/9; with two busy threads in the container: 20/20; the four controls 8/8 and 8/8. Chromium: the proof and its controls 10/10.
  • The release note names the snap paging Carousel (32e21eb), CodeBunny's 16:52Z item: a plain hasSnap keeps native CSS scroll snap, and only hasSnap={useSnapPaging()} runs the pager whose settle now follows tempo (Carousel.tsx's hasSnap docs), so the changeset names that value. The same commit drops tempo from the pager gap in spec:AST-094's FR45 verification row, which the tempo fix made stale.

  • The instant-page check judges at least 12 frames (3ee5207): the check that Next and the arrow key show the next photo at once sampled the track for 600 ms and needed more than 10 frames, and WebKit in a 4-CPU container painted 3 to 6 under load. It now samples for 600 ms and at least 12 frames, gives up at half the test's timeout with "too few frames to judge", and keeps the one-place check as it was.

  • A released page settles at the theme's tempo (22da53d): the spec owners ruled this a consistency fix: spec:AST-108 plays motion at the theme's tempo, and spec:AST-094 requires the touch presentations to follow it, but the pager's settle after a release ignored it. solvePaging now takes the tempo. Each step lasts tempo times as long and carries tempo times the release's distance, so a settle from rest keeps its shape and takes tempo times as long, and a release still starts at 96 % of the finger's speed. At tempo 1 the settle is unchanged. No motion role names the paging table, so usePager reads the tempo of the theme the track is in. Every pager follows it: a Lightbox gallery, and a Carousel given hasSnap={useSnapPaging()} (on main), which the new changeset names; a plain hasSnap is native CSS scroll snap and does not change. goTo's animated scroll stays spec:AST-094's separate gap.

    • A hard release overshoots a little more at a slower tempo, as a spring does: a 3,000 px/s release overshoots its page by 3.2 px at the default tempo and by 17.8 px at the slowest tempo a theme allows (1.5). Only the test's tempo 3 gives 104.6 px.
    • Red first: the paging.test.ts case for a settle's shape and length at tempo 0.75, 1.5 and 3 fails on the previous head's code, and so does usePager.test.ts "a release's settle plays at the theme's tempo 3". The two other new paging.test.ts cases are guards that pass on the old code: a release's first speed at any tempo (a first speed left unscaled fails it) and tempo 1 unchanged.
    • The busy-main-thread proof of the gallery's settle plays at tempo 3, as the zoom's does. The slowed settle runs first, on a fresh track, and the page starts a 600 ms block on it; then the same swipe runs at tempo 1. The slowed settle is three times as long (2.9 to 3.1), still runs when the block ends, and keeps pace with the compositor's control animation through the block. A frozen settle and a settle that the main thread drives both fail the proof. On the previous head's build the proof fails 5/5 in WebKit and 3/3 in Chromium, on the length ratio (1.00).
    • "A drag the owner refuses springs back over frames" now reads the spring-back from its own keyframes instead of counting main-thread frames, which a loaded runner thins out. It failed 3/10 under the WebKit CI profile on the previous head; it passes 10/10 on both builds now.
  • A single zoomed photo gives past its edge again (91d4a1e), CodeBunny's 10:27Z item: the zoom stage's pan refused a finger's drag headed past a zoomed photo's edge in every Lightbox, so a single photo resting at its edge no longer rubber-banded there. The stage now yields at the edge only when Lightbox says an outer pager is there (hasOuterPager: a gallery under a finger). The new case "a single zoomed photo's drag outward from the edge it rests at rubber-bands and returns" passes on main and at this head, and fails on the previous head, 3 of 3 in Chromium and in WebKit. The record's press-tracker line no longer names the stage's own pointerdown listener.

  • The zoom pan's begin threshold (b8266e6): the zoom stage's pan takes its begin threshold per press from the shared PanGestureRecognizer's hysteresis function, and Lightbox's own capture pointerdown listener on the stage goes. A mouse's or a pen's drag on a zoomed photo still pans from its first move, and a finger keeps UIKit's threshold. New unit case: "takes the zoom pan's begin threshold from the shared pan, with no pointerdown listener of its own on the stage".

    • Pinch and pan start, against 0fc7062, same frames:
      • Chromium with touch emulation, 2 runs × 5 per head: the pinch starts at 9 px of spread and 150 ms on both heads; the pan on a 2× photo starts at 16 px of travel and 266.8 ms, with a first move of 6 px, on both heads.
      • Safari on the iOS 26.2 simulator, 4 takes per head, alternating: a two-finger pinch scales the photo on its first two-finger move (one simulator step, 20–24 pt of spread) on both heads, and the pan after it starts at the first move past 10 pt (11 pt in 1 pt steps) on both heads.
    • A fine-pointer test's comment splits one 26-word sentence in two (architecture:knowledge-contracts INV29). The same sentence in feat(Lightbox): touch zoom like the iOS Photos viewer #8148's LightboxZoom spec gets its split when this branch is restacked on feat(Lightbox): touch zoom like the iOS Photos viewer #8148's current head.
    • At b8266e6: Lightbox units 129/129; core tsc 0; strict ESLint 0 on the three files.
  • Red-first: "a drag that starts on a video's controls stays the video's; a drag on its frame pages" fails without the band: the drag on the controls pages the gallery. It passes in Chromium and WebKit.

  • At this head: New cases pin the mouse path through the pager's option: "with a mouse the Next button shows the next photo at once" and "with a mouse a Next the owner refuses never moves the photo", each sampling the row's offset every frame. "a pinch on a zoomable gallery's caption still zooms the page" (Chromium; WebKit's driver drops the first finger) fails without the zoom stage's caption check: the photo's scale read 1.72 beside the page's 1.72. "a video that ends during a drag on its frame leaves the drag to page" guards the drag through the video's ended.

  • A refused drag springs back: "with a mouse a drag the owner refuses springs back" samples the track every frame from the release. It fails with this head's usePager reverted: the photo jumps from past half a page to rest in one frame, as CodeBunny measured (−877 px to 0).

  • Browsers: Chromium runs the Lightbox and Carousel a11y specs 30/30 and the Lightbox and usePager interaction specs 84/84; WebKit runs the same interaction specs 78/78. Each engine skips the cases its driver cannot send (4 in Chromium, 10 in WebKit), and the other engine runs them. The video band runs in both. Without the band, WebKit's drag on a video's controls pages the gallery; Chromium's own control panel already keeps that drag.

  • Size (INV6): Lightbox loads its touch code with it, as Toast and Slider do, by the spec owners' decision that touch code stays eager. The Lightbox import, bundled with every module it reaches (React external) and gzipped at the default level, grows from 51,394 bytes on main (dc90b50) to 99,585 at this head, the zoom and paging beneath included. Most of it is the shared touch modules a page loads once: minified, the gestures grow by 77.6 KB, motion by 41.3 KB and the shared hooks by 10.1 KB, while Lightbox's own code grows by 14.1 KB. The package stylesheet grows by 45 bytes gzip (40,267 → 40,312). No dependency changes.

  • Elements (INV7): an open 4-photo "Gallery" at its first photo renders 28 elements on main and 34 at this head. The added six: the paging track; the current page's box and its media box (the zoom stage); and the neighbouring page the drag reveals (its box, media box and photo). Each page keeps its one caption, as main keeps its one.

  • Fine pointer, before and after (FR59): desktop Chromium, a mouse drag from x 900 to 300 across "Gallery" leaves it on 1 / 4 on main and on this head; a trackpad's sideways scroll leaves it there too (WebKit). The strips this item showed before were of the earlier, every-pointer paging.

    main: main: a mouse drag moves nothing

    this pull request: the row follows the mouse and pages

  • Fine pointer, the looks the range carries from feat(Lightbox): touch zoom like the iOS Photos viewer #8148 (FR59): desktop Chromium, "With zoom" over a light photo.

    • The control backing: on main the photo never lies under Close, so it has none; here a double-click zooms the photo under Close, which sits on its dark backing.

      main: main: Close over the backdrop, no backing this pull request: Close on its backing over the zoomed photo

    • The zoom target's focus ring, from the keyboard: on main it traces the photo's box; here the stage fills the viewer, so the ring traces the viewer's edge, inside it.

      main: main: the ring around the photo box this pull request: the ring along the viewer's edge

  • Fine pointer, the zoom settle the range carries from feat(Lightbox): touch zoom like the iOS Photos viewer #8148 (FR59): with hasZoom, Enter on the zoom target and a mouse double-click zoom on the shared springs.settle that feat(Lightbox): touch zoom like the iOS Photos viewer #8148 gives every zoom, where main plays a 200 ms ease-out. Measured at each exact head in desktop Chromium (1280 × 800, mouse and keyboard, no touch; default theme, tempo 1; "With zoom" with a 3000 × 2000 photo, 1× to 2×), by pausing the running animation and sampling it every 2 ms:

    Head 50 % 90 % 99 % Within 0.1 %
    main (1dce318), Enter or double-click 70 ms 150 ms 186 ms 196 ms (a 200 ms ease-out)
    this head (0fc7062), Enter or double-click 134 ms 310 ms 530 ms 736 ms

    Enter and the double-click measure the same at each head, two runs each.

Safari on the iOS 26.2 simulator, from the previous head (the paging is unchanged here), a swipe left:

main this pull request, with the main thread blocked 600 ms after the lift
main: nothing moves the next photo settles through the block

The page settle's busy proof tracks the next photo as it slides in, against a compositor-only control animation, and passes in both engines.

Testing

At 0bb42a8 on main 5c1b7d4: core tsc is clean, where 51bf10b alone has the two errors #10464 brings, and the Lightbox unit files pass 129/129. The Lightbox interaction specs pass 73 in Chromium (3 skips) and 69 in WebKit in a 4-CPU container, the CI profile (7 skips), and the Lightbox a11y specs pass 26 in Chromium and 24 in WebKit (2 skips).

At 3963547 on main 993e1fa: core tsc is clean and the Lightbox unit files pass 129/129. "A zoomed photo keeps the drag until its edge; the next drag from the edge pages" fails in WebKit in a 4-CPU container, the CI profile, with 6 busy threads: 15 of 15 at cc382a8 and 5 of 5 at 17e7968. The photo left behind stays at scale 2.53. With the fix it passes 15 of 15 with 6 busy threads, and on cc382a8 15 of 15 with none. The Lightbox interaction specs pass 73 in Chromium (3 skips) and 69 in WebKit (7 skips), and the Lightbox a11y specs pass 26 in Chromium and 24 in WebKit (2 skips).

At cc382a8 on main 84a2254: core tsc is clean; the busy double-tap zoom proof and its two controls pass 12 of 12 in WebKit in a 4-CPU container, the CI profile, with 0 and with 6 busy threads, and 12 of 12 in Chromium; with the proof at slowViewer's default tempo 3, its ratio check fails 2 of 2 in each engine ("Expected: > 4.9, Received: 3"); the Lightbox interaction specs pass 73 in Chromium (3 skips) and 69 in WebKit (7 skips).

At e0eab2a on main a888b3d: core tsc is clean; the Lightbox interaction specs pass 73 in Chromium (3 skips) and 69 in WebKit in a 4-CPU container, the CI profile (7 skips).

At d3bd5ce on main 20ab992: core tsc is clean; pnpm check:changesets and pnpm check:knowledge pass; the Lightbox, usePager, paging, SnapPaging and Carousel unit files pass 322/322. In WebKit in a 4-CPU container (the CI profile), the busy proof of a page's settle passes 3/3, and the Lightbox interaction specs pass 69 (7 skips).

At 258ac0d on main 755206d (it merges clean with main aaa33df): core tsc is clean; ESLint is clean; comment sentence lengths 0 over. The busy proof of a page's settle passes 9/9 in WebKit in a 4-CPU container (the CI profile) and 20/20 with two busy threads in that container, and its four controls fail as designed, 8/8 in each; in Chromium the proof and its controls pass 10/10. The whole LightboxPaging, LightboxZoom and LightboxZoomModes files pass 56/56 in Chromium and 52 (4 skips) in WebKit in that container. On a merge with main aaa33df, as CI builds it, the busy proof passes 3/3 in the CI profile and 6/6 with two busy threads.

At 3ee5207 on main 1c142dc: pnpm install --frozen-lockfile exits 0; the root build and the Storybook build pass; core tsc is clean; the Lightbox, usePager and paging unit files pass 283/283. The spring-zoom checks ("the keyboard’s zoom settles on springs.settle" and "a mouse double-click settles on springs.settle too") pass 30/30 in Chromium under a 2-CPU container quota with 2 workers and 20/20 in WebKit in a 4-CPU container (the CI profile). The instant-page check passes 20/20 in each; its old sampler failed 10/10 in that WebKit container, and a track that shows one intermediate place fails it in both engines. The whole LightboxFinePointer a11y file passes 21/21 in Chromium under that quota and 19 (2 skips) in WebKit. The three Chromium counts were re-run under the quota at 258ac0d, with the same results.

At 22da53d on main bef866e:

  • pnpm install --frozen-lockfile exits 0; core tsc is clean; the motion, hooks, Lightbox, SnapPaging, Carousel and gestures unit files pass 2,956/2,956; pnpm check:knowledge and pnpm check:changesets pass; comment sentence lengths 0 over.
  • Main's only change under these files since 2151c88 is two lines of the spec:AST-094 record, so the browser runs on the same two fix commits on 2151c88 (dc015a2) stand: the Lightbox, usePager, SnapPaging and Carousel interaction specs pass 118 in WebKit in a 4-CPU container (the CI profile) and 118 in Chromium (10 skips each); the Lightbox and Carousel a11y specs pass 42/42 in Chromium.
  • The tempo-3 busy proof of a page's settle and its two controls pass 30/30 in the CI profile and 9/9 in Chromium; on the previous head's build the proof fails 5/5 and 3/3, on the settle's length (ratio 1.00).
  • "A drag the owner refuses springs back over frames" passes 10/10 in the CI profile on both builds (3/10 on the previous head before its fix).
  • "A single zoomed photo's drag outward from the edge it rests at rubber-bands and returns" passes on main and at this head, and fails 3/3 in Chromium and in the CI profile on the previous head's build.

Earlier, at dc3a360:

At dc3a360, this pull request's own lines are byte-identical to 878f6b8's (the head below moved). At 878f6b8, product code and tests are byte-identical to 7f5fcc1 and 42cb8bf apart from the record and two test files, and these ran on them:

At dc3a360 on box 6: pnpm install --frozen-lockfile 0; core tsc 0; strict ESLint on every changed file 0; CI=true pnpm lint 0 errors; the Lightbox, motion, gestures and hooks unit files 2,650/2,650; pnpm check:knowledge and pnpm check:changesets pass; a root build and a Storybook build of this head, then Chromium Lightbox a11y specs 24/24, Chromium Lightbox interaction specs 67/67 (3 WebKit-only skips), WebKit 63/63 (7 Chromium-only skips).

  • pnpm install --frozen-lockfile exits 0; core tsc is clean, and so is strict ESLint on every changed file; CI=true pnpm lint shows no error; INV29 finds no long sentence; pnpm check:knowledge passes;
  • the Lightbox, usePager, motion/uikit and gestures unit files pass 1302/1302;
  • a root build and a Storybook build of 42cb8bf, then: Chromium Lightbox a11y specs 24/24 (with this head's spec files); Chromium Lightbox and hook interaction specs 89/89 (5 WebKit-only skips); WebKit 84/84 (10 Chromium-only skips);
  • red first on 6a6505e's build: "in a gallery, a mouse click beside a large photo, level with it, leaves the viewer open, as on main" and "without zoom, a mouse click at the far edge beside a large gallery photo leaves the viewer open, as on main"; and the unit "stays open on a mouse click beside a large photo, level with it, since its media spans the viewer" with 2b36122's Lightbox.tsx.

Earlier, at 2b36122 on #8148's 33e0568 (main 5a373df): #9810's gate at 86d0dd1, which holds this tree plus #9810, passed: Chromium Lightbox a11y specs 17/17; Chromium Lightbox interaction specs 67/67 (3 WebKit-only skips); WebKit 63/63 (7 Chromium-only skips); the Lightbox and usePager unit files 254/254. The checks below ran at 99cfba1, this change before #8148's trackpad gate, a changeset line, a reflowed comment and two test titles.

  • pnpm install --frozen-lockfile exits 0; core tsc is clean, and so is strict ESLint on every changed file; pnpm lint shows the same 171 warnings as the previous head and no error; INV29 finds no long sentence;
  • the Lightbox, usePager, motion/uikit and gestures unit files pass 1290/1290; pnpm check:knowledge passes;
  • a root build and a Storybook build of this tree, then: Chromium Lightbox a11y specs 17/17; Chromium Lightbox interaction specs 65/65 (2 WebKit-only skips); WebKit 60/60 (7 Chromium-only skips);
  • red first on d585c45's build: "a mouse drag sideways leaves a gallery on its photo, as on main" (Chromium and WebKit), "a trackpad's horizontal scroll leaves a gallery on its photo" (WebKit), and "a gallery leaves its sideways touches to the page where the primary pointer is fine" (its viewport claimed pan-y pinch-zoom);
  • red first on c358801's build: "without zoom, a mouse press beside the photo that moves 60 px before its release / is held 1.6 s before its release closes the gallery, as on main"; on a NOT-FOR-LANDING arm without the click's detail check (5641fb2), the unit "leaves the viewer open when the keyboard activates a caption link after a mouse press";
  • red first with the previous head's stage click (48f02af): on a NOT-FOR-LANDING arm with its Lightbox.tsx (14548d2), the units "closes on a mouse press beside the photo that is released on it" and "stays open on a mouse click beside a narrow photo, within its caption's width"; on a build whose stage click code is that head's, the browser cases "…a mouse press beside the photo that is released on it closes the gallery, as on main" and "…a mouse click beside a photo narrower than its caption leaves the gallery open, as on main";
  • on main's Storybook and on this build alike, in the Gallery story: a press beside the photo released on it closes the viewer; a click beside a 128 × 768 photo, within its 273 px caption's width, leaves it open; a press on the photo released beside it closes it;
  • lint, typecheck and the full unit suites run before this leaves draft.

Before merge, the human check on a phone tries a scrubber drag on iOS Safari and Android Chrome: it stays with the video, and a drag on the frame pages. If either fails, the video clause becomes a named gap and this ships photo paging only.

@vercel

vercel Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Nov 7, 2026 7:06pm UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 24, 2026
@astracat-bot

astracat-bot Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

AI review status for this pull request.

Review status Updated
❓ Needs maintainer input (for maintainers only) Nov 7, 2026, 11:42 PM UTC

@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

🔎 Preview · 0bb42a8

Storybook · Sandbox · built from this PR's head

Modified components

Component Size LoC Visual · A11y · RTL
lab/Comments 🔴 330.6 kB (+7.3 kB) · 2.5 kB CSS (0) — — · — · —
lab/CommentThread 🔴 325.2 kB (+6.8 kB) · 2.4 kB CSS (0) — — · — · —
Lightbox · Storybook 🔴 116.6 kB (+6.5 kB) · 10.4 kB CSS (+66 B) 2,977 (+505) ✓ · ✓ · ✓
31 more components
Component Size LoC Visual · A11y · RTL
BottomSheet 65.0 kB (0) · 7.5 kB CSS (0) — — · — · —
ContextMenu 105.9 kB (0) · 17.0 kB CSS (0) — — · — · —
DateInput 181.3 kB (0) · 16.7 kB CSS (0) — — · — · —
DateTimeInput 203.5 kB (0) · 18.4 kB CSS (0) — — · — · —
DropdownMenu 94.4 kB (0) · 17.1 kB CSS (0) — — · — · —
Markdown 178.5 kB (0) · 20.0 kB CSS (0) — — · — · —
MoreMenu 94.7 kB (0) · 17.1 kB CSS (0) — — · — · —
MultiSelector 92.7 kB (0) · 16.6 kB CSS (0) — — · — · —
Pagination 107.4 kB (0) · 17.5 kB CSS (0) — — · — · —
Popover 87.2 kB (0) · 13.9 kB CSS (0) — — · — · —
PowerSearch 280.7 kB (0) · 23.5 kB CSS (0) — — · — · —
ProgressBar 11.7 kB (0) · 6.2 kB CSS (0) — — · — · —
SegmentedControl 91.9 kB (0) · 6.8 kB CSS (0) — — · — · —
Selector 97.4 kB (0) · 17.1 kB CSS (0) — — · — · —
Slider 104.9 kB (0) · 8.7 kB CSS (0) — — · — · —
Switch 99.5 kB (0) · 9.9 kB CSS (0) — — · — · —
Table 116.1 kB (0) · 17.7 kB CSS (0) — — · — · —
TableCell 107.2 kB (0) · 17.4 kB CSS (0) — — · — · —
TableHeaderCell 106.7 kB (0) · 17.2 kB CSS (0) — — · — · —
TimeInput 157.3 kB (0) · 15.7 kB CSS (0) — — · — · —
Tokenizer 89.5 kB (0) · 18.3 kB CSS (0) — — · — · —
lab/ChatEmojiPicker 101.7 kB (0) · 773 B CSS (0) — — · — · —
lab/ChatReactionBar 102.8 kB (0) · 1.1 kB CSS (0) — — · — · —
lab/CommentComposer 143.7 kB (0) · 1.9 kB CSS (0) — — · — · —
lab/NavigationScreen 79.2 kB (0) · 1.5 kB CSS (0) — — · — · —
lab/NavigationStack 79.2 kB (0) · 1.5 kB CSS (0) — — · — · —
lab/PageControl 72.4 kB (0) · 709 B CSS (0) — — · — · —
lab/TabBar 73.1 kB (0) · 1.8 kB CSS (0) — — · — · —
lab/TabBarItem 3.8 kB (0) · 1.8 kB CSS (0) — — · — · —
lab/TourStep 93.5 kB (0) · 590 B CSS (0) — — · — · —
richtext/RichTextEditorToolbar 249.3 kB (0) · 454 B CSS (0) — — · — · —

Package size: @astryxdesign/core 6.92 MB packed (+11.3 kB); no new dependencies.

API changes: none in this pull request

Committed API report behind this build: cli 21 rows, core 33 rows; run pnpm api:report.

Touched components checked; add ci:run-visual or ci:run-rtl to run the full suites.


Generated by PR Enrichment workflow | View full report

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approval waits on an engineering-owner decision: swipe paging for Lightbox galleries rests on the draft spec:AST-094 (FR1–FR23, DEC-1, DEC-2) and on a draft component:Lightbox record that does not cover paging, so an engineering owner needs to approve or reject that direction, including its proposed amendments to architecture:theme-tokens, family:buttons, architecture:interaction-modality and spec:AST-025, by commenting /approve-spec <full head SHA> on this pull request (the author may do this as an engineering owner). Whatever that decision, these fixes are needed:

  • Make gallery pages that are not current inert and aria-hidden, since Tab can currently reach a link in the next item's caption, scrolling that item into view under the previous item's counter, and screen readers read the neighbours' captions.
  • Re-observe the zoom stage when the current page changes, because two items away from the opening one a rotation no longer re-fits a zoomed photo.
  • Stop usePager from reporting controlled index changes from an Effect (architecture:react-component-runtime INV2 and INV3) and return the track when a controlled index does not follow a swipe.
  • Land the stacked pull requests first so this one carries only its own change instead of about 31,000 lines.

[Automated review]

Comment thread packages/core/src/Lightbox/Lightbox.tsx Resolved
Comment thread packages/core/src/hooks/usePager.ts Resolved

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approval waits on an engineering owner's decision on the draft touch-presentations spec (AST-094) that this pull request introduces as the authority for Lightbox swipe paging, including whether buttons and arrow keys keep sliding under Reduce Motion where its FR8 asks for a cross-fade. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner. Whatever the decision, these need fixing:

  • Pages beside the current photo stay focusable and exposed: Tab reaches photo 3's caption link and scrolls it into view under “2 / 4”.
  • A controlled parent that refuses a swipe is left with the swiped photo on screen.
  • Only the photo the dialog first rendered re-fits when a zoomed photo is rotated.
  • usePager reports controlled index changes from an Effect.
  • The diff carries #8045, #8146 and #8148 (31,552 authored lines); rebase or retarget it so it holds only the paging step.

[Automated review]

Comment thread packages/core/src/Lightbox/Lightbox.tsx Resolved

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approval is waiting on an engineering owner's decision about Lightbox swipe paging and the draft spec:AST-094 that would govern it: an engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request (the author may do so if they are an engineering owner), or paging waits for a current Lightbox spec. Whatever that decision, these need changes:

  • Make the off-screen gallery pages inert, so screen readers and Tab no longer reach captions and controls that are not on screen.
  • Move the pager back to index when a controlled parent refuses a swipe, and stop calling onIndexChange from the controlled-sync Effect.
  • Report Lightbox's base-against-head gzip size (the import grows by about 31 KB gzip) and load the touch-only code lazily or justify it.
  • Re-observe the zoom stage when the current page changes, regenerate the CLI docs golden that fails test-node (1/4), and rebase onto the stacked pull requests so this one carries only the paging commit.

[Automated review]

@cixzhang

Copy link
Copy Markdown
Contributor

Posted by Cindy's agent on her behalf.

Parts of this PR re-implement behavior that already exists, or is being specified, as a shared part of core. Converging keeps one implementation per behavior:

  • Reduced motion. core/src/hooks/usePager.ts:353 handles reduced motion locally. Per design:reduced-motion (DR1–DR4), the animator is the one place that resolves it (touch cross-fades in 200 ms or less with no travel, a fine pointer changes at once), and core has no opt-out, so reducedMotion: 'keep' should be removed.

This is about where the behavior lives, not about the PR's intent.

@vjeux

vjeux commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

Agreed: one implementation.

@github-actions github-actions Bot added the needs:design-review Affects visuals — Design should review label Oct 25, 2026
@vjeux

vjeux commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

Pushed in 2834675, on #8146 @ f6b1827, which has no 'keep', and #8320 @ 41b46ea. The gallery passes no reduced-motion option of its own.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approval is waiting on an engineering owner's decision about Lightbox swipe paging and the draft touch-presentations spec (AST-094) proposed to govern it: an engineering owner either approves that spec and the paging direction by commenting /approve-spec <full head SHA> on this pull request (the author may do so as an engineering owner), or paging waits for a current Lightbox spec. That decision has to settle AST-094's requirements, including FR4's rule that touch motion is not themable while this branch adds theme motion roles the pager reads, and how callers keep the fine-pointer behaviour on a touch device. Whatever the decision, these need changes:

  • Pause a gallery video when the viewer moves away from it; today a video the person started keeps playing, unseen and unreachable, on the neighbouring page.
  • Split the UIKit gesture table so Lightbox ships only the rows it reads, and report base and head sizes with the CSS delta, loading the touch-only code lazily or justifying its cost.
  • Remove the platform adaptation axis from the theme template, since defineTheme rejects it.
  • Rebase onto the stacked pull requests so this one carries only the paging commit, and split out the astryxStylex undefinedVariables option.

[Automated review]

Comment thread packages/core/src/Lightbox/Lightbox.tsx Resolved
Comment thread packages/cli/assets/theme.template.ts Resolved
@cixzhang

cixzhang commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 22da53d

@astracat-bot astracat-bot Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested: touch paging follows the current Lightbox spec (FR6, FR7), mouse and keyboard users keep today's behaviour, and the spec text this pull request adds is approved for this revision. One fix remains:

  • .changeset/pager-settle-tempo.md says a released page settles at the theme's tempo "in a Carousel with hasSnap", but hasSnap on its own is native CSS scroll snap and does not change, so the release note should say "in a Carousel given hasSnap={useSnapPaging()}".
  • Before merge: the branch now conflicts with main in LightboxFinePointer.a11y.browser.spec.ts, where #10301 rewrote the expectSpringZoom comment; merging main and keeping its comment resolves it.

Non-blocking: the GalleryWithCaptions story's comment says the photo keeps one box whatever captions render, while each page now sizes its photo above its own caption. CI has not run on this revision yet; a maintainer adds the ci:run label so it runs.

[Automated review]

Comment thread .changeset/pager-settle-tempo.md Resolved
};

/**
* A gallery whose captions differ in length: the photo keeps one box

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: each page now lays out its own photo above its own caption (Lightbox.tsx:250-258), so a page with a longer caption gets a smaller photo box, as the description notes. This comment says the photo keeps one box whatever captions render; rewording it to what the story shows keeps it accurate.

vjeux added 3 commits November 6, 2026 09:02
The release note named a Carousel with hasSnap, but a plain hasSnap is
native CSS scroll snap. Only hasSnap={useSnapPaging()} runs the pager
whose settle now follows tempo, so the note names that value. The
AST-094 FR45 verification row no longer lists tempo as a gap of the
pager's settle.
This merge takes main at 1c142dc. meta#10301 rewrote the spring-zoom
check in LightboxFinePointer.a11y.browser.spec.ts onto the shared
interaction harness, and this pull request had split a sentence of
that check's old comment. The merge keeps meta#10301's check and its
comment as they are on main.
The check that Next and the arrow key show the next photo at once
samples the track every frame for 600 ms after the press, and it needs
more than 10 frames. WebKit in a 4-CPU container painted 3 to 7 frames
in that time under load, so the check failed there. The sampler now
runs for 600 ms and at least 12 frames. At half the test's timeout it
gives up, and too few frames fail by name. The one-place check is the
same.
@cixzhang

cixzhang commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 3ee5207

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved pending CI: CI has not run on this revision yet, so a maintainer adds the ci:run label so it runs.

Touch paging follows the current Lightbox spec (FR6, FR7), mouse and keyboard users keep today's behaviour, and the spec text this pull request adds is approved for this revision. The earlier changeset wording is fixed: the tempo note now names a Carousel given hasSnap={useSnapPaging()}.

Non-blocking: the GalleryWithCaptions story's comment still says the photo keeps one box whatever captions render, while each page now sizes its photo above its own caption.

[Automated review]

vjeux added 2 commits November 6, 2026 12:10
This merge takes main at 755206d. Main's busy-proof harness now
judges each run of a pooled proof on its own and leaves out, by name, a
run whose control presented nothing. The page-settle proof that follows
relies on it.
One settle moves visibly for about 550 ms at tempo 3. While the main
thread is blocked, WebKit's software compositor presents a frame every
110 to 200 ms, so a settle held three to five frames. A loaded runner
presented three, under the proof's four, and the proof failed as too
little to judge, though the photo had moved on every one of them. A
starved compositor could also draw one or two frames before it applied
a settle handed over just before the block.

The proof now judges six settles, to page 4 and back, as one pool, at
tempo 5. A run whose control presented nothing is left out by name, and
the pool needs three judged runs. Each block starts one rendering update
after the handoff, once the compositor has drawn the settle, and each
run skips at most one frame the compositor began before the block. A
settle that stays still for two frames or more into the block still
fails, as two new controls prove.
@cixzhang

cixzhang commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 258ac0d

Main's v0.7.10 release consumed .changeset/lightbox-every-pointer.md,
the note for meta#8148's ctrl-wheel and trackpad zoom, which this pull
request had edited to read "with `hasZoom`". The merge keeps main's
deletion and drops the edit: on main the zoom stage already runs only
for a photo with `hasZoom`, so the qualifier names no change of this
pull request. Its own notes, lightbox-gallery-paging.md and
pager-settle-tempo.md, are unchanged.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pull request is reviewed as part of its stack; see the review on #10284.

@cixzhang

cixzhang commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

/approve-spec d3bd5ce

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pull request is reviewed as part of its stacks; see the reviews on #10284 and #8215.

Main's meta#10406 added recordLiftOffset to LightboxZoom's interaction
spec, beside this pull request's slowViewer import. The merge keeps
both imports; every other path is the automatic merge.
@cixzhang

cixzhang commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

/approve-spec e0eab2a

@astracat-bot astracat-bot Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pull request is reviewed as part of its stack; see the review on #9810.

Main's meta#10415 plays LightboxZoom's busy double-tap proof at tempo 5
through `slowZoom`, which this pull request replaced with the shared
`slowViewer`. The merge keeps meta#10415's three constants and calls
`slowViewer(page, ZOOM_TEMPO)` in the proof and its controls; every
other path is the automatic merge.
@cixzhang

cixzhang commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

/approve-spec cc382a8

@astracat-bot astracat-bot Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pull request is reviewed as part of its stack; see the review on #9810.

vjeux added 2 commits November 6, 2026 21:28
The restack of meta#8197 on main 993e1fa, which brings meta#10436's
PowerSearch play fix for the story play guard. No conflict; this pull
request's own diff is unchanged.
The pager's onSettle reset every photo outside the page marked current.
On a slow runner the settle ends before the render that moves that
mark, so the photo left behind was still marked current and kept its
zoom. Each page now carries its index, and the reset skips only the
page the pager names.
@cixzhang

cixzhang commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 3963547

@astracat-bot astracat-bot Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pull request is reviewed as part of its stacks; see the reviews on #9810 and #8215.

vjeux added 2 commits November 7, 2026 08:16
The restack of meta#8197 on main 5c1b7d4, which brings meta#10464: a busy
pool now leaves out a run that began too late, as null. No conflict.
Main's busy pool now leaves out a run that began too late, as null in
its runs. The page settle proof reads each judged run's monotonic flag,
and its delayed control reads the first run's frame interval. A judged
run must exist, so a missing one fails that check. A first run the pool
left out fails by name, with the pool's summary, before the control
uses its interval.
@cixzhang

cixzhang commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 0bb42a8

@astracat-bot astracat-bot Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pull request is reviewed as part of its stacks; see the reviews on #10284 and #8215.

This branch was successfully deployed

1 active deployment
Preview — 0bb42a85 Deployed Nov 7, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) ci:run-a11y Run the accessibility suite on this PR; write access, next push (AST-030 FR12) CLA Signed This label is managed by the Meta Open Source bot. needs:design-review Affects visuals — Design should review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants